介绍一下事件循环
宏任务和微任务
前提
JS是单线程的,JS是通过事件队列(Event Loop)的方式来实现异步回调的,怎么拥有的,下面我们从进程、线程的角度来解释
CPU
计算机的核心是CPU,它承担了所有的计算任务。
它就像一座工厂,时刻在运行。
进程
进程就好比工厂的车间,它代表CPU所能处理的单个任务。 进程之间相互独立,任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。 CPU使用时间片轮转进度算法来实现同时运行多个进程。
线程
一个车间里,可以有很多工人,共享车间所有的资源,他们协同完成一个任务。
线程就好比车间里的工人,一个进程可以包括多个线程,多个线程共享进程资源。
CPU、进程、线程之间的关系
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
- 不同进程之间也可以通信,不过代价较大
- 单线程与多线程,都是指在一个进程内的单和多
浏览器是多进程的
- 浏览器是都进程的
- 每个Tab页,就是一个独立的进程
浏览器包含哪些进程
主进程
- 协调控制其他子进程(创建、销毁)
- 浏览器界面显示,用户交互,前进,后退,收藏
- 将渲染进程得到的内存中Bitmap,绘制到用户界面上
- 处理不可见操作,网络请求,文件访问等
第三方插件进程
- 每种类型的插件对应一个进程,仅当使用该插件时才创建
GPU进程
- 用于3D绘制等
渲染进程,就是我们说的浏览器内核
- 负责页面渲染,脚本执行,事件处理等
- 每个tab页一个渲染进程
浏览器内核(渲染进程)
GUI渲染线程
- 负责渲染页面,布局和绘制
- 页面需要重绘和回流时,该线程就会执行
- 与js引擎线程互斥,防止渲染结果不可预期
JS引擎线程
- 负责处理解析和执行javascript脚本程序
- 只有一个JS引擎线程(单线程)
- 与GUI渲染线程互斥,防止渲染结果不可预期
事件触发线程
- 用来控制事件循环(鼠标点击,setTimeOut, ajax等)
- 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中
定时触发线程
- setInterval与setTimeout所在的线程
- 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
- 计时完毕后,通知事件触发线程
异步http请求线程
- 浏览器有一个单独的线程用来处理AJAX请求
- 当请求完时,若由回调函数,通知事件触发线程
为什么javascript是单线程的
首先是历史原因,在创建 javascript 这门语言时,多进程多线程的架构并不流行,硬件支持并不好。
其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高
作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质
为什么GUI渲染进程和JS引擎线程互斥
因为JS可以操作DOM,如果改变元素属性并同时渲染界面(JS线程和UI线程同时运行), 那么渲染线程前后获得的元素就不一致了
因此,为了防止渲染出现不可预期的结果,浏览器设定GUI渲染进程和JS引擎线程为互斥关系, 当JS引擎线程执行时GUI渲染线程会被挂起,GUI跟新则会被保存在一个队列中等待JS引擎线程空闲时立即被执行
从Event Loop看JS的运行机制
- js分为同步任务和异步任务
- 同步任务都在JS引擎线程上执行,形成一个执行栈
- 事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中
- 执行栈中所有的同步任务执行完毕时,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行
我们知道,不管是setTimeout/setInterval和XHR/fetch代码,在这些代码执行时,
本身是同步任务,而其中的回调函数才是异步任务。当代码执行到setTimeout/setInterval时,实际上是JS引擎线程通知定时触发器线程,间隔一个时间后,会触发一个回调事件,而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中。
当代码执行到XHR/fetch时,实际上是JS引擎线程通知异步http请求线程,发送一个网络请求,并制定请求完成后的回调事件,而异步http请求线程在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中。
当我们的同步任务执行完,JS引擎线程会询问事件触发线程,在事件队列中是否有待执行的回调函数,如果有就会加入到执行栈中交给JS引擎线程执行
1 | let timerCallback = function() { |
总结一下:
- JS引擎线程只执行执行栈中的事件
- 执行栈中的代码执行完毕,就会读取事件队列中的事件
- 事件队列中的回调事件,是由各自线程插入到事件队列中的
- 如此循环
宏任务
我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。
我们前文提到过JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染
1 | // 宏任务-->渲染-->宏任务-->渲染-->渲染... |
主代码块,setTimeout,setInterval等,都属于宏任务
1 | document.body.style = 'background:black'; |
我们会看到的结果是,页面背景会在瞬间变成灰色,以上代码属于同一次宏任务,所以全部执行完才触发页面渲染,渲染时GUI线程会将所有UI改动优化合并,所以视觉效果上,只会看到页面变成灰色。
1 | document.body.style = 'background:blue'; |
我会看到,页面先显示成蓝色背景,然后瞬间变成了黑色背景,这是因为以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成蓝色,然后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色。
微任务
微任务可以理解为在当前宏任务执行后立即执行的任务
也就是说,当宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完
Promise、process.nextTick等,属于微任务
1 | document.body.style = 'background:blue' |
输出 1, 3, 2, 页面直接变成黑色
1 | console.log('1'); |
1, 7, 6, 8, 2, 4, 3, 5, 9, 11, 10, 12
通过自己画执行任务队列,分宏任务,微任务,
每次执行完一个宏任务,去执行所有微任务,
微任务嵌套宏任务时,等到执行那个微任务,再将宏任务加入执行队列里面,每次只看最外层是个什么任务,当作一个整体
1 | console.log('1'); |
2,4, 3, aa, bb